Skip to content

feat(worker): cohort skip-guards — no-op every team-iterating job for is_test_cohort teams (PR-1 follow-up) - #90

Merged
mastermanas805 merged 2 commits into
masterfrom
feat/worker-cohort-skip-guards
Jun 4, 2026
Merged

feat(worker): cohort skip-guards — no-op every team-iterating job for is_test_cohort teams (PR-1 follow-up)#90
mastermanas805 merged 2 commits into
masterfrom
feat/worker-cohort-skip-guards

Conversation

@mastermanas805

Copy link
Copy Markdown
Member

What

Worker-side follow-up to api #246 (W0). teams.is_test_cohort (migration 067, already on master) tags durable synthetic test-cohort teams. This PR makes every background job that charges / churns / emails / quota-nudges a team SKIP an is_test_cohort team, so continuous synthetic monitoring never pollutes the real funnel / billing / email surface.

Plan: docs/sessions/2026-06-04/TEST-ACCOUNTS-AND-NR-SYNTHETICS-PLAN.md §1.6.

How — two skip surfaces, both UNCONDITIONAL + inert

No real team is is_test_cohort=true today (migration 067 defaults every row false; only a future seeder flips the synthetic teams). So every guard is a pure no-op for all real teams — a safety guard, not a feature. It deliberately does NOT depend on FLOW_SYNTHETIC_ENABLED (that flag gates the future synthetic RUNNER, not these guards).

Shared helper internal/jobs/test_cohort.go:

  • testCohortNotExistsClause(teamIDExpr) — NULL-safe NOT EXISTS SQL fragment for scans whose driving table carries a team_id but does not already join teams. (NOT EXISTS, not NOT IN, so anonymous team_id IS NULL rows are never silently dropped.)
  • isTestCohort / skipIfTestCohort — per-team Go guard for loops. Fail-safe-for-real-teams: on a DB error it returns "process normally", so a platform-DB blip can never silently suppress a real customer's charge/email.

Jobs guarded (§1.6) — each with its skip mechanism

Job (file) Mechanism Why
quota.go (suspend / unsuspend / redis-eviction scans) AND testCohortNotExistsClause("r.team_id") on all 3 resources scans synthetic over-quota provisions would suspend + emit resource.quota_suspended email
quota_wall_nudge.go AND NOT is_test_cohort on the teams scan fake 80% upsell emails + audit noise
churn_predictor.go AND NOT t.is_test_cohort on the teams candidate scan skews churn scores + we-miss-you email
expire_imminent.go AND testCohortNotExistsClause("r.team_id") resource.expiry_imminent warning email
expiry_reminder.go AND testCohortNotExistsClause("r.team_id") anon.expiry_warning 12/6/1h warning email
billing_reconciler.go AND NOT is_test_cohort (primary teams sweep) + AND NOT t.is_test_cohort (orphan pending_checkouts sweep) seeded paid teams have no real Razorpay sub → flagged as drift / undeliverable charge
checkout_reconcile.go AND testCohortNotExistsClause("pc.team_id") checkout.abandoned email
payment_grace_reminder.go AND testCohortNotExistsClause("pgp.team_id") payment.grace_reminder dunning email
payment_grace_terminator.go AND testCohortNotExistsClause("pgp.team_id") calls api terminate + payment.grace_terminated email
email.go (weekly_digest) AND NOT t.is_test_cohort on the users JOIN teams scan digest.weekly email

Deliberately NOT touched (scope boundary, pinned by TestNotGuardedJobs_AreStillPresent)

  • expire.go — TTL reaper, not an emailer. Must reap synthetic anon/free RESOURCES so they never leak (plan: keep TTL-expiry of test-cohort resources intact). Only acts on tier IN ('anonymous','free'); seeded synthetic teams are paid-tier and out of scope anyway.
  • expire_stacks.go — TTL reaper for anonymous stacks (NULL team_id; synthetic deploys are anon /stacks/new per project memory). Must tear down synthetic stacks so namespaces never leak; no customer email.
  • lifecycle_emails.go — pure Go email render layer (no DB scan / team iteration); gated upstream by the guarded trigger jobs.
  • deployment_expirer.go / deployment_reminder.go — not in the §1.6 enumeration. Deploy expiry is a reaper (keep teardown intact); these are out of the stated scope. Synthetic deploys are anon stacks reaped by expire_stacks; seeded paid teams don't auto-create TTL deploys. Left for a follow-up if synthetic deploy coverage is added.

Tests (failing-then-passing verified)

Removed two guards locally → TestIntegration_QuotaWallNudge_SkipsTestCohort red (cohort got a nudge) + TestAllTeamIteratingJobs_FilterTestCohort red for both files → restored → green.

  • test_cohort_test.go — sqlmock unit coverage of the helper (flagged / not-flagged / row-vanished / DB-error). 100% of test_cohort.go.
  • test_cohort_registry_test.gorule-18 enumeration net: TestAllTeamIteratingJobs_FilterTestCohort reds if any enumerated job loses its guard; TestNotGuardedJobs_AreStillPresent pins the scope boundary.
  • test_cohort_integration_test.go — real-Postgres round-trips proving each SQL-driven job skips a cohort team while processing a normal team: TestIntegration_{QuotaWallNudge,ChurnPredictor,ExpiryReminder,ExpireImminent,WeeklyDigest,CheckoutReconcile,PaymentGraceReminder,BillingReconciler}_SkipsTestCohort. DB-gated (skips no-DB lanes).

testhelpers: added is_test_cohort + users / pending_checkouts / payment_grace_periods subset tables (idempotent) + cohort seed/read helpers. Two-gate note: the harness change is idempotent + DB-gated, so both ci.yml (-race, no DB) and deploy.yml (-short, no DB) stay green.

Coverage block

Symptom:        a team-iterating job charges/churns/emails/quota-nudges a synthetic team
Enumeration:    plan §1.6 + grep "FROM teams / JOIN teams / team_id" across internal/jobs/
Sites found:    10 guarded (quota, quota_wall_nudge, churn_predictor, expire_imminent,
                expiry_reminder, billing_reconciler[primary+orphan], checkout_reconcile,
                payment_grace_reminder, payment_grace_terminator, email/weekly_digest)
Sites touched:  10 (3 reapers/render layer deliberately skipped + documented)
Coverage test:  TestAllTeamIteratingJobs_FilterTestCohort (reds if a guard rots out)
Live verified:  make gate GREEN; failing-then-passing proven by guard removal

Gate

make gate GREEN (build + vet + go test ./... -short -count=1 — the exact deploy.yml test step). test_cohort.go 100% function coverage. Touched files gofmt'd via $(go env GOROOT)/bin/gofmt.

🤖 Generated with Claude Code

… is_test_cohort teams (PR-1 follow-up)

Worker-side follow-up to api #246 (W0): teams.is_test_cohort (migration 067,
already on master) tags durable synthetic test-cohort teams. This makes every
background job that charges / churns / emails / quota-nudges a team SKIP an
is_test_cohort team, so continuous synthetic monitoring never pollutes the real
funnel / billing / email surface.

Plan: docs/sessions/2026-06-04/TEST-ACCOUNTS-AND-NR-SYNTHETICS-PLAN.md §1.6.

Mechanism — two skip surfaces, both UNCONDITIONAL + inert (no real team is
is_test_cohort=true today, so behaviour is unchanged for all real teams; this
is a safety guard, not a feature, and does NOT depend on FLOW_SYNTHETIC_ENABLED):
  - SQL scans add `AND NOT [t.]is_test_cohort` (teams-joined) or a NULL-safe
    NOT EXISTS subselect (testCohortNotExistsClause, for team_id-carrying scans).
  - Per-team Go loops can call isTestCohort/skipIfTestCohort (shared helper).

Guarded (§1.6): quota.go (suspend/unsuspend/redis-eviction scans),
quota_wall_nudge.go, churn_predictor.go, expire_imminent.go, expiry_reminder.go,
billing_reconciler.go (primary + orphan sweep), checkout_reconcile.go,
payment_grace_reminder.go, payment_grace_terminator.go, email.go (weekly_digest).

Deliberately NOT guarded (documented in test_cohort_registry_test.go): expire.go
+ expire_stacks.go (TTL REAPERS — must reap synthetic resources/stacks so they
never leak; no customer email), lifecycle_emails.go (pure render layer, gated by
the guarded trigger jobs upstream).

Tests:
  - test_cohort_test.go: sqlmock unit coverage of the helper (flagged /
    not-flagged / row-vanished / DB-error fail-safe-for-real-teams). 100% of
    test_cohort.go.
  - test_cohort_registry_test.go: rule-18 enumeration net —
    TestAllTeamIteratingJobs_FilterTestCohort fails if any enumerated job loses
    its guard, plus TestNotGuardedJobs_AreStillPresent pins the scope boundary.
  - test_cohort_integration_test.go: real-Postgres round-trips proving each
    SQL-driven job skips a cohort team while processing a normal team
    (quota_wall_nudge, churn_predictor, expiry_reminder, expire_imminent,
    weekly_digest, checkout_reconcile, payment_grace_reminder,
    billing_reconciler). Gated by SetupTestDB (skips no-DB lanes).

testhelpers: add is_test_cohort + the users/pending_checkouts/
payment_grace_periods subset tables + cohort seed/read helpers. The two-gate
note (ci.yml + deploy.yml) holds: the harness change is idempotent and DB-gated
so both -short, no-DB workflows stay green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@mastermanas805
mastermanas805 enabled auto-merge (squash) June 4, 2026 20:10
…#90 coverage)

cohort.go is exercised only cross-package by internal/jobs integration tests, so
the per-package coverage job reported it 0% (94 missing lines) → 100%-patch gate
red. Add an in-package smoke test (DB-gated via SetupTestDB, runs in coverage.yml)
bringing all 8 seed/query helpers to 100%, mirroring billing_deletion_smoke_test.go.
Recurring testhelpers gotcha (3rd: #87/#89/#90) — see memory.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@mastermanas805
mastermanas805 merged commit 55c7b9f into master Jun 4, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant